# --------------------------------------------------------------------------------------------------------------------
# Project:             Digital Payment System (DPS)
# Website:             http://code.google.com/p/dps-x509/
# Purpose of document: This class constructs an x.509 certificate
# --------------------------------------------------------------------------------------------------------------------
import sys, string, logging, base64, os
from time import time

sys.path.append("..")

from pyasn1.type import base, univ
from pyasn1 import error

try:
  import keyczar.keyinfo as keyinfo
  import keyczar.keys as keys
  import keyczar.util as util
except ImportError:
  from keyczar import *

import dps.asn1CertificateSpecification
import dps.errors
import dps.x509Asn1Extractor
import dps.x509AttributeAndValue
import dps.x509CertificateParams
import dps.x509Extension
import dps.x509Helper
import dps.utils

# --------------------------------------------------------------------------------------------------------------------
TEMPLATE_FILE = """
-----BEGIN CERTIFICATE-----
MIIECjCCAvKgAwIBAgIFAJCZl1EwDQYJKoZIhvcNAQEFBQAwgZUxCzAJBgNVBAYT
AkFVMQwwCgYDVQQIEwNBQ1QxETAPBgNVBAcTCENhbmJlcnJhMSIwIAYDVQQKExlL
ZW4gQ2VydGlmaWNhdGUgQXV0aG9yaXR5MRswGQYDVQQLExJJc3N1aW5nIERlcGFy
dG1lbnQxFjAUBgoJkiaJk/IsZAEBEwYxMjM0NTYxDDAKBgNVBAMTA0tlbjAeFw0x
MDA0MjIwNTM5MDRaFw0xMzA0MjIwNTM5MDRaMIIBEjELMAkGA1UEBhMCQVUxDDAK
BgNVBAgTA0FDVDERMA8GA1UEBxMIQ2FuYmVycmExGDAWBgNVBAoTD0JpZ1Bob25l
Y29tcGFueTEcMBoGA1UECxMTQ3VycmVuY3kgRGVwYXJ0bWVudDETMBEGCgmSJomT
8ixkAQETAzEyMzEnMCUGA1UEAxMeUmVnaXN0cnk6dXJyZW1vdGUuY29tL3JlZ2lz
dHJ5MSwwKgYDVQQDEyNSZWRlZW1lcjpCaWdQaG9uZUNvbXBhbnkuY29tL2NyZWRp
dDEQMA4GA1UEAxMHQ29kZTpNTTEUMBIGA1UEAxMLUXVhbnRpdHk6MTAxFjAUBgNV
BAMTDU1vYmlsZU1pbnV0ZXMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBALuB
qpQOoAUFpW25tbRZSPcXeD3MmuZ4ri8j0Sl2EyWwFaBSkRFvFAB8b4UIfggH/L9S
liYx0xRrXJUU/qa2boXknO5r+hIPwfYTQcAZxSHQPbs/UJvr7Sk5kd7Ya0dvuBJz
mXEAC73PWqCAKk1xo8HWerGWwpeqdXdpQ3/jubmvAgMBAAGjZTBjMBQGCWCGSAGG
+EIBAQEB/wQEAwIEsDAqBgNVHSUBAf8EIDAeBggrBgEFBQcDAgYIKwYBBQUHAwMG
CCsGAQUFBwMEMA4GA1UdDwEB/wQEAwIDuDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG
SIb3DQEBBQUAA4IBAQAPDT0uvnNVN8KLAldvQhRl76Guk6JK32dyq243pVAtc7Up
IZykmQKarfCz9COf/rq5SOlDpyaKCip3rx2PZOX7s/DzK11awXa5nE5YVhEeBZSs
+V/Ul4XINebW2ziGZCoOT366Yi9vogwOYQxLWX54jxoy+qC4TbnhyWDmAJGn9Z8m
KvnVV3nLxkjtohvLd0CF0gwxjxcHYqYJYlsZEHbXleH63LyaatEOWusVp8XEv452
HBV5L+JtGHgRBJ0CCIgbUgpms0k+mFYvY+fpTCnOzHVgx3TweF0tjLxHSCh7GfJ9
WA5+IcTEjNEnpAzAFeKIf2DOfCHMjwrebLhHnmNJ
-----END CERTIFICATE-----
"""

# --------------------------------------------------------------------------------------------------------------------
def generatePrivateKey(size= keyinfo.RSA_PRIV.default_size):
  """ create an keys.RsaPrivateKey 
      this can take up to 15 seconds. might need to think about how to cache keys or something else
      @param  size: size of key. 
      @todo:  check that size it is multiple of 256
      @raise  dps.errors.RsaPrivateKeyCreateError: if it fails
      @rtype: keyczar.keys.RsaPrivateKey """
  privateKey = None
  try:      
    #this is all just in here for testing...
    t0 = time()
    privateKey = keys.RsaPrivateKey.Generate()
    t1 = time()
    dps.utils.out("private key generation took %f seconds" %(t1 - t0))
    pk = str(privateKey)
  except Exception, e:      
    dps.utils.out("Exception in x509Asn1Creator.generatePrivateKey: " + str(sys.exc_info()[0]) + str(e))      
    raise dps.errors.RsaPrivateKeyCreateError()

  return privateKey

# --------------------------------------------------------------------------------------------------------------------
class X509Asn1Creator:
  """ 
  Responsible for making ASN.1 data from python objects. We cheat a bit here. 
  Rather than building a document from scratch we take a copy of a template certificate and modify
  its contents. This was largely due to time constraints

  important conversions to know:

  We are dealing with 3 main datatypes:
    - Asn1:   U{http://en.wikipedia.org/wiki/Asn1}
    - Bytes:  U{http://en.wikipedia.org/wiki/Bytes}
    - Base64: U{http://en.wikipedia.org/wiki/Base64}
  """    

# --------------------------------------------------------------------------------------------------------------------
  def __getSubjectPublicKeyAsn(self, privateKey):
    """ create the private keys matching public key """
    mod = util.BytesToLong(privateKey.public_key.params["modulus"])
    publicExp = util.BytesToLong(privateKey.public_key.params["publicExponent"])
    params = {"n": mod, "e": publicExp}
    subjectPublicKeyInfoAsn = dps.x509Helper.bytesToAsn1(dps.x509Helper.base64ToBytes(util.ExportRsaX509(params), True), dps.asn1CertificateSpecification.SubjectPublicKeyInfo())
    return subjectPublicKeyInfoAsn

  # --------------------------------------------------------------------------------------------------------------------
  def __getTemplateCertificateAsn(self):
    """ copy an existing certificate from file """
    certs = dps.x509Helper.certsToBase64(TEMPLATE_FILE)
    dps.utils.assertion(len(certs) == 1, \
                        "x509Asn1Creator.__getTemplateCertificateAsn: Could not find exactly one certificate")
    certBytes = dps.x509Helper.base64ToBytes(certs[0])
    certAsn = dps.x509Helper.bytesToAsn1(certBytes, dps.asn1CertificateSpecification.Asn1CertificateSpecification())
    return certAsn

  # --------------------------------------------------------------------------------------------------------------------
  def __setSubjectPublicKeyInfo(self, subjectPublicKeyInfoAsn):
    """ 
    sets the public key info for the certifcate 
    @param subjectPublicKeyInfoAsn: public key info
    @type  subjectPublicKeyInfoAsn: base.Asn1Item 
    """
    dps.utils.assertion(isinstance(subjectPublicKeyInfoAsn, base.Asn1Item), \
                        "X509Asn1Creator.__setSubjectPublicKeyInfo: Incorrect subjectPublicKeyInfoAsn type")

    tbsCertificateAsn = self.__certAsn_.getComponentByName("tbsCertificate")
    tbsCertificateAsn.setComponentByName("subjectPublicKeyInfo", subjectPublicKeyInfoAsn)
    self.__certAsn_.setComponentByName("tbsCertificate", tbsCertificateAsn)

  # --------------------------------------------------------------------------------------------------------------------
  def __setIssuerAsn(self, issuerAsn1):
    """ 
    sets the issuer for the certificate
    @param issuerAsn1: asn in asn1 format 
    """
    dps.utils.assertion(isinstance(issuerAsn1, base.Asn1Item), \
                        "X509Asn1Creator.__setSubjectPublicKeyInfo: Incorrect issuerAsn1 type")
    self.__certAsn_.getComponentByName("tbsCertificate").setComponentByName("issuer", issuerAsn1)   

  # --------------------------------------------------------------------------------------------------------------------
  def __setSubjectAsn(self, subjectAsn1):
    """ sets the subject for the certificate
    @param subjectAsn1: subject in asn1 format """
    dps.utils.assertion(isinstance(subjectAsn1, base.Asn1Item), \
                        "X509Asn1Creator.__setSubjectPublicKeyInfo: Incorrect subjectAsn1 type")
    self.__certAsn_.getComponentByName("tbsCertificate").setComponentByName("subject", subjectAsn1)   
    
  # --------------------------------------------------------------------------------------------------------------------
  def __addExtensions(self, params):
    """ add an extension to the params """
    dps.utils.assertion(isinstance(params, dps.x509CertificateParams.X509CertificateParams), \
                        "X509Asn1Creator.__addExtensions: Incorrect params type")    
    
    extensionAsn1 = self.__certAsn_.getComponentByName("tbsCertificate").getComponentByName("extensions")
    dps.utils.out("before: "+dps.x509Helper.bytesToBase64(dps.x509Helper.asn1ToBytes(extensionAsn1)), 1)
    extensions = dps.x509Extension.X509ExtensionList.loadFromAsn1(extensionAsn1)
    
    #set token type
    if params.tokenType_:
      tokenOid = dps.x509Extension.stringRepresentationToExtensionOid("tokenType")
      tokenExt = dps.x509Extension.X509Extension(params.tokenType_, tokenOid)
      extensions.addItem(tokenExt)

    #set token value
    if params.tokenQuantity_:
      tokenOid = dps.x509Extension.stringRepresentationToExtensionOid("tokenQuantity")
      tokenExt = dps.x509Extension.X509Extension(params.tokenQuantity_, tokenOid)
      extensions.addItem(tokenExt)
    
    #set redeemer
    if params.redeemer_:
      tokenOid = dps.x509Extension.stringRepresentationToExtensionOid("redeemer")
      tokenExt = dps.x509Extension.X509Extension(params.redeemer_, tokenOid)
      extensions.addItem(tokenExt)
  
    #set registrar
    if params.registrar_:
      tokenOid = dps.x509Extension.stringRepresentationToExtensionOid("registrar")
      tokenExt = dps.x509Extension.X509Extension(params.registrar_, tokenOid)
      extensions.addItem(tokenExt)
      
    #set exchanger
    if params.exchanger_:
      tokenOid = dps.x509Extension.stringRepresentationToExtensionOid("exchanger")
      tokenExt = dps.x509Extension.X509Extension(params.exchanger_, tokenOid)
      extensions.addItem(tokenExt)
    
    #set id
    if params.id_:
      tokenOid = dps.x509Extension.stringRepresentationToExtensionOid("id")
      tokenExt = dps.x509Extension.X509Extension(params.id_, tokenOid)
      extensions.addItem(tokenExt)
      
    #set posInChain
    if params.posInChain_:
      tokenOid = dps.x509Extension.stringRepresentationToExtensionOid("posInChain")
      tokenExt = dps.x509Extension.X509Extension(params.posInChain_, tokenOid)
      extensions.addItem(tokenExt)
    
    newExtensionAsn1 = extensions.asAsn1()
    dps.utils.out("after: "+dps.x509Helper.bytesToBase64(dps.x509Helper.asn1ToBytes(newExtensionAsn1)), 1)
    self.__certAsn_.getComponentByName("tbsCertificate").setComponentByName("extensions", newExtensionAsn1)
    
# --------------------------------------------------------------------------------------------------------------------
  def __init__(self, params, privateKey= None):
    """ 
    create a certificate with params
    @param params: The contents of params are inserted into the certificate
    @type  params: dps.x509.certificateParams.X509CertificateParams
    @param privateKey: if None it will be created for you
    @type  privateKey: keyczar.keys.RsaPrivateKey 
    @raise dps.errors.RsaPrivateKeyCreateError: if unable to create the certifcate 
    """
    dps.utils.assertion(isinstance(params, dps.x509CertificateParams.X509CertificateParams), \
                        "X509Asn1Creator.__init__: Incorrect params type")
    #this next assert would be nice by i haven't worked out python namespace resolution yet...
    #dps.utils.assertion(not privateKey or isinstance(params, keys.RsaPrivateKey), \
    #                    "X509Asn1Creator.__init__: Incorrect privateKey type")    
    try:
      self.__certAsn_    = None
      self.__isSigned_   = False
      self.__privateKey_ = privateKey or generatePrivateKey()    

      subjectPublicKeyInfoAsn = self.__getSubjectPublicKeyAsn(self.__privateKey_)

      #copy from template
      self.__certAsn_ = self.__getTemplateCertificateAsn()

      #change any of the fields here
      if not params.getIssuer():
        raise dps.errors.MissingRequiredParametersError("No issuer data")      
      self.__setIssuerAsn(params.getIssuer().asAsn1())

      if not params.getSubject():
        raise dps.errors.MissingRequiredParametersError("No subject data")      
      self.__setSubjectAsn(params.getSubject().asAsn1())

      self.__addExtensions(params)
      
      #add the new public key to the certificate
      self.__setSubjectPublicKeyInfo(subjectPublicKeyInfoAsn)

    except dps.errors.DPSBaseError:
      raise     
    except Exception, e:      
      dps.utils.out("Exception in X509Asn1Creator.__init__: " + str(sys.exc_info()[0]) + str(e))      
      raise dps.errors.CertificateCreateErrorError()

  # --------------------------------------------------------------------------------------------------------------------
  def getAsn1(self):
    """ 
    get entire certificate in asn1 format
    @raise dps.errors.CertificateNotSignedError: if the certificate has yet to be signed 
    @rtype: asn1
    """
    if self.__isSigned_:
      return self.__certAsn_
    else:
      raise dps.errors.CertificateNotSignedError()


  # --------------------------------------------------------------------------------------------------------------------
  def getBase64(self):
    """ 
    get entire certificate in base64 format
    @raise dps.errors.CertificateNotSignedError: if the certificate has yet to be signed 
    @rtype: string 
    """
    if self.__isSigned_:
      certBase64 = dps.x509Helper.bytesToBase64(dps.x509Helper.asn1ToBytes(self.__certAsn_))

      dps.utils.assertion(isinstance(certBase64, str), \
                          "X509Asn1Creator.getBase64: Incorrect certBase64 type")
      return certBase64
    else:
      raise dps.errors.CertificateNotSignedError()

# --------------------------------------------------------------------------------------------------------------------
  def signWithPrivateKey(self, privateKey):
    """ 
    signs the certificate with keyczar.keys.RsaPrivateKey. this includes adding the signed data into the cert asn1
    @raise dps.errors.RsaPrivateKeySigningError: if unable to sign certificate
    @rtype: void
    """
    dps.utils.assertion(isinstance(privateKey, keys.RsaPrivateKey), \
                        "X509Asn1Creator.signWithPrivateKey: Incorrect privateKey type")

    try:
      tbsCertificateAsn = self.__certAsn_.getComponentByName("tbsCertificate")
      certBytes = dps.x509Helper.asn1ToBytes(tbsCertificateAsn)
      sigBytes = privateKey.Sign(certBytes)
      sigBin = util.BytesToBin(sigBytes)
      signatureValue = univ.BitString("'%s'B" % sigBin)  # needs to be a BIT STRING  

      self.__certAsn_.setComponentByName("signatureValue", signatureValue)
      self.__isSigned_ = True
    except Exception, e:      
      dps.utils.out("Exception in X509Asn1Creator.signWithPrivateKey: " + str(sys.exc_info()[0]) + str(e))      
      raise dps.errors.RsaPrivateKeySigningError()

# --------------------------------------------------------------------------------------------------------------------
  def getRsaPrivateKey(self):
    """ 
    get the keyczar.keys.RsaPrivateKey from the certificate
    @rtype: keyczar.keys.RsaPrivateKey
    """
    return self.__privateKey_

# --------------------------------------------------------------------------------------------------------------------
if __name__ == "__main__":  
  try:

    privateKeyStr = """{"primeExponentP": 
    "ANe7sE57jbmYMNCvsgpZ0yhf9LPASbTv0dGCXL8KE9qTJRpkW-F56l56Kph-YTI
     1wM1lGedrAUmxpygK34ID8yMyv1qEI-f1017Rz1rmjERGzD3RDQlqkBeR_diOl0
     6LYWTq9NQLh-1C_5pfB5BpMQXuPzpeGfmQlLt_pJIgS7VB", 
     "primeExponentQ": 
    "AHYSYRiyfum_Q4yyjD8fe2TANG6bIq7idpnkG7dpFZNcMQRZpDx0qmu3bTNFTSf
    6fl0b4_LUUWN7xi0FHtWCCAzeG9tvgSqrZre0qXQNPUuCxeTS7xF4R8DGXLC8NpV
    S4N46txwyi1jHsf2f-A9qPEtzPbCfz8-UxRk92_Q8BCWL", 
    "crtCoefficient": 
    "AIIgyy5F4CmrUai1NHGCvuB5GcnEjdkoaqqXbLQtWw1ILgSm6quSVE3N0-723_D
    _LuJEdT5zSAKfCSfHy5MU5W-nY9Nb6uftWzcSvEcY42sbsyHkRJfE_wQSqAr3e7w
    _iot0iMcRlGVy84CbSw5zwaE2EOPjECcVcxzq7IOFi0pT", 
    "privateExponent": 
    "ABlMjag9ug_WmUogKUOT02b8VEeRD9ytEv2KcHlKsN7VHkYSC4CJIrI0pTO0dJF
    NdGnO64Xncut33Ng55aU6hk8UuvwjmMQEm2uoZccJFWnUeYSHyZRqyr5CymUCCK1
    eWekie8tOvrgUaRYG6SOkFwFeb_sNH33IfUtf0qw-UKV-Nj6ct8ASE_M9IQwndV4
    qVFnBxhAqbMSq8tCoYwJVvJ0bOduDu1K7a_ujqlLqNdQjrnnFV4GlG1ZnpaXleCi
    4h3Zbwe2miCwM4xvhhh6lJ52xu3rewcjDYDUv0s4Mv5r30WN2Q1Wr46gYrfz_cVf
    0bxVaymspDR5lOJ1ZI2_X8YE", 
    "publicKey": 
    {"publicExponent": "AAEAAQ", 
    "modulus": 
    "AJ3codzWcyHSpKO7JymiCawALVVEvr6UIxCVtJSQP8yrCOzsRWHtBLAovSaTXy
    ytlammrWqo7ovUCXWS5h4t3E6O7kCnbC_OdVfELCbfhTnir8LN6gUHtMI_07tfk
    A9hdDoeAK1o4cWgoO2ABCgywyx4DOffTac6vkKRdxGMiajdpCvSJf_k-qXe_9A4
    yTG87hOqsfQhCD08D7WkOdfuBvZ300fYjYPDnUlS2x8jAG8p8bwGP1NWSUomGHZ
    cBaex_1HP0qiO49Xux2Ot_zgKMmf7Jh2XjawswUTx9RlGoIOzvUBNwqBChjPUGT
    OCxgZd4Y4JA24KUsXF5zvRJ2250Hs", "size": 2048}, 
    "primeQ": 
    "AKYMqc7rWRapU3kBL6_Q8iydlJi8wwdDXQAeh8jIx6JbgwnBJbDpDDbKcD7yVx
    P_7MrwshGGiohXunq1bPoWm8yMnTsZt9bNYrIghD37GKRofjcU8ic7CKWgw0EH5
    r0U1nK72aZYV9NSccEnn1sfEydm5Jp74SreWGwr0oD5lIu7", 
    "primeP": 
    "APNgh498oKgdw8yVpZd_jzKN8geFsXyATQAMnPlKzJ_1tFfgZ6wEBKxApIGLYQ
    aH0d3bM2OeOgWlDsY__hUkHkL7U69WR_rcTpVm7b791ScSAPafr5jPcKrZY9z-s
    MtJDM_JnG4U5YYCHDfIe9gdFzlXbh9WcsIuM5FgC4AjYqJB", 
    "size": 2048}"""
    privateKey = keys.RsaPrivateKey.Read(privateKeyStr.replace("\n",""))

    params = dps.x509CertificateParams.X509CertificateParams()
    params.isCa_ = True

    #create issuer
    issuer = dps.x509AttributeAndValue.X509AttributeAndValueList()
    issuerItem = dps.x509AttributeAndValue.X509AttributeAndValue("test")
    issuer.addItem(issuerItem)
    params.setIssuer(issuer)
    
    params.tokenType_ = "My token"
    params.tokenQuantity_ = 14.21
    params.redeemer_ = "test@hotmail.com"
    params.registrar_ = "test@hotmail.com"
    
    #use same subject (self signed)
    params.setSubject(issuer)
    c = X509Asn1Creator(params, privateKey)
    c.signWithPrivateKey(c.getRsaPrivateKey())

    certBase64 = c.getBase64()
    print certBase64

  except Exception, e:
    dps.utils.out("An exception occured: " + str(sys.exc_info()[0]) + " " + str(e))


